/*
 * File:    AbstractMemoryBuffer.java
 * Created: 30-Oct-2007
 *
 * by Kenji Hollis <kenji@bitgatesoftware.com>
 * Released under the Creative Commons License version 2.5
 */

package com.bitgate.util.buffer;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;

/**
 * This is an abstract implementation of a memory-based buffer.  These classes are automatically used by {@link BufferFactory}
 * and should not be instantiated by the programmer directly.
 * 
 * @author Kenji Hollis &lt;kenji@bitgatesoftware.com&gt;
 * @since 0.1
 */
public class AbstractMemoryBuffer implements BufferInterface {

	private final AtomicReference<byte[]> data = new AtomicReference<byte[]>();
	private long limit;
	
	/**
	 * No-argument standard constructor.
	 */
	public AbstractMemoryBuffer() {
		limit = 0L;
		data.set(null);
	}
	
	/**
	 * Constructor used to copy data from the persisted disk layer back to the memory buffer.  This is called by the
	 * {@link AbstractDiskBuffer}, and is not available for standard use in the {@link BufferFactory} class.
	 * 
	 * @param newData <code>byte[]</code> containing the data to copy back.
	 */
	public AbstractMemoryBuffer(byte[] newData) {
		limit = 0L;
		data.set(newData);
	}
	
	public void appendData(byte[] appendData) throws BufferException {
		if (limit > 0) {
			if (data.get() == null) {
				if (appendData.length > limit) {
					throw new BufferException("Buffer limit exceeded: limit is " + limit + " byte(s)");
				}
			} else {
				if (data.get().length + appendData.length > limit) {
					throw new BufferException("Buffer limit exceeded: limit is " + limit + " byte(s)");
				}
			}
		}
		
		if (data.get() != null) {
			byte[] newData = new byte[data.get().length + appendData.length];
			
			System.arraycopy(data.get(), 0, newData, 0, data.get().length);
			System.arraycopy(appendData, 0, newData, data.get().length, appendData.length);
			
			data.set(newData);
		} else {
			data.set(appendData);
		}
	}

	public void clear() {
		data.set(null);
	}
	
	public byte[] get() {
		return data.get();
	}
	
	public byte[] get(long size) {
		if (data.get() == null) {
			return null;
		} else if (size > data.get().length) {
			return get();
		}
		
		byte[] returnData = new byte[(int) size];
		
		System.arraycopy(data.get(), 0, returnData, 0, (int) size);
		
		return returnData;
	}

	public byte[] take() {
		byte[] returnData = data.get();
		
		data.set(null);
		
		return returnData;
	}

	public byte[] take(long size) {
		if (data.get() == null) {
			return null;
		} else if (size > data.get().length) {
			return take();
		}
		
		byte[] returnData = new byte[(int) size];
		byte[] newData = new byte[data.get().length - (int) size];
		
		System.arraycopy(data.get(), 0, returnData, 0, (int) size);
		System.arraycopy(data.get(), (int) size, newData, 0, (data.get().length - (int) size));
		
		data.set(newData);
		
		return returnData;
	}

	public long getLimit() {
		return limit;
	}

	public void insertData(byte[] insertData) throws BufferException {
		if (limit > 0) {
			if (data.get().length + insertData.length > limit) {
				throw new BufferException("Buffer limit exceeded: limit is " + limit + " byte(s)");
			}
		}
		
		if (data.get() != null) {
			byte[] newData = new byte[data.get().length + insertData.length];
			
			System.arraycopy(insertData, 0, newData, 0, insertData.length);
			System.arraycopy(data.get(), 0, newData, insertData.length, data.get().length);
			
			data.set(newData);
		} else {
			data.set(insertData);
		}
	}

	/**
	 * If the buffer inside this object is persisted, this function returns a {@link AbstractDiskBuffer} object.
	 * 
	 * @return <code>BufferInterface</code> implemented by an {@link AbstractDiskBuffer}.
	 */
	public BufferInterface persistToStorage() throws BufferException {
		BufferInterface adInterface = null;
		
		try {
			adInterface = new AbstractDiskBuffer(get());
		} catch(IOException e) {
			throw new BufferException("Unable to persist buffer to disk.", e);
		}
		
		deallocate();
		
		return adInterface;
	}

	public BufferInterface retrieveFromStorage() throws BufferException {
		throw new BufferException("Unable to retrieve storage interface for memory buffer.");
	}

	public void setLimit(long size) throws BufferException {
		if (data.get() != null) {
			if (size < data.get().length) {
				throw new BufferException("Cannot set buffer size less than the current buffer size of " + data.get().length +
					" byte(s).");
			}
		}
		
		if (size < 0) {
			throw new IllegalArgumentException("Buffer size cannot be less than 0.");
		}
		
		limit = size;
	}

	public long size() {
		if (data.get() == null) {
			return 0L;
		}
		
		return data.get().length;
	}
	
	public void deallocate() throws BufferException {
		data.set(null);
	}
	
}